package com.basic.ui.view.image

import android.text.TextUtils
import android.util.Log
import androidx.annotation.VisibleForTesting
import com.basic.ui.view.image.monitor.ImageMonitor
import com.bumptech.glide.Priority
import com.bumptech.glide.load.DataSource
import com.bumptech.glide.load.HttpException
import com.bumptech.glide.load.data.DataFetcher
import com.bumptech.glide.load.model.GlideUrl
import com.bumptech.glide.util.ContentLengthInputStream
import com.bumptech.glide.util.LogTime
import com.bumptech.glide.util.Synthetic
import kotlinx.coroutines.*
import java.io.ByteArrayInputStream
import java.io.IOException
import java.io.InputStream
import java.net.HttpURLConnection
import java.net.URISyntaxException
import java.net.URL
import java.nio.ByteBuffer

class HttpUrlFetcher @VisibleForTesting @JvmOverloads constructor(
    private val glideUrl: GlideUrl,
    private val timeout: Int,
    private val connectionFactory: HttpUrlConnectionFactory = DEFAULT_CONNECTION_FACTORY
) : DataFetcher<InputStream> {
    @Volatile
    private var isCancelled = false
    private var callback: DataFetcher.DataCallback<in InputStream>? = null
    private var job:Job?= null


    override fun loadData(
        priority: Priority,
        callback: DataFetcher.DataCallback<in InputStream>
    ) {
        this.callback = callback
        job =  GlobalScope.launch(CoroutineName(TAG)) {
            withContext(Dispatchers.IO){
                val startTime: Long = LogTime.getLogTime()
                val startTimeNew = System.currentTimeMillis()
                try {
                    val url: URL = glideUrl.toURL()
                    val result = loadDataWithRedirects(url, 0, null, glideUrl.getHeaders())
                    if (result != null) {
                        callback.onDataReady(result)
                    } else {
                        callback.onLoadFailed(Exception("Canceled"))
                    }
                } catch (e: Exception) {
                    val msg = "Failed to load :" + e.message
                    imageMonitor(
                        null,
                        startTimeNew,
                        System.currentTimeMillis(),
                        0,
                        glideUrl.toStringUrl(),
                        false,
                        msg
                    )
                    if (Log.isLoggable(HttpUrlFetcher.TAG, Log.DEBUG)) {
                        Log.d(HttpUrlFetcher.TAG, "Failed to load data for url", e)
                    }
                    callback.onLoadFailed(e)
                } finally {
                    if (Log.isLoggable(HttpUrlFetcher.TAG, Log.VERBOSE)) {
                        Log.v(
                            HttpUrlFetcher.TAG,
                            "Finished http url fetcher fetch in " + LogTime.getElapsedMillis(startTime)
                        )
                    }
                }
            }
        }
    }

    @Throws(IOException::class)
    private fun loadDataWithRedirects(
        url: URL, redirects: Int, lastUrl: URL?,
        headers: Map<String, String>
    ): InputStream? {
        if (redirects >= MAXIMUM_REDIRECTS) {
            throw HttpException("Too many (> $MAXIMUM_REDIRECTS) redirects!")
        } else {
            // Comparing the URLs using .equals performs additional network I/O and is generally broken.
            // See http://michaelscharf.blogspot.com/2006/11/javaneturlequals-and-hashcode-make.html.
            try {
                if (lastUrl != null && url.toURI() == lastUrl.toURI()) {
                    throw HttpException("In re-direct loop")
                }
            } catch (e: URISyntaxException) {
                // Do nothing, this is best effort.
            }
        }
        val urlConnection = connectionFactory.build(url)
        for ((key, value) in headers) {
            urlConnection.addRequestProperty(key, value)
        }
        urlConnection.connectTimeout = timeout
        urlConnection.readTimeout = timeout
        urlConnection.useCaches = false
        urlConnection.doInput = true

        // Stop the urlConnection instance of HttpUrlConnection from following redirects so that
        // redirects will be handled by recursive calls to this method, loadDataWithRedirects.
        urlConnection.instanceFollowRedirects = false
        val startTime = System.currentTimeMillis()
        // Connect explicitly to avoid errors in decoders if connection fails.
        urlConnection.connect()
        // Set the stream so that it's closed in cleanup to avoid resource leaks. See #2352.
        if (isCancelled) {
            return null
        }
        val statusCode = urlConnection.responseCode
        return try {
            if (isHttpOk(statusCode)) {
                getStreamForSuccessfulRequest(urlConnection, startTime)
            } else if (isHttpRedirect(statusCode)) {
                val redirectUrlString = urlConnection.getHeaderField("Location")
                if (TextUtils.isEmpty(redirectUrlString)) {
                    throw HttpException("Received empty or null redirect url")
                }
                val redirectUrl = URL(url, redirectUrlString)
                loadDataWithRedirects(redirectUrl, redirects + 1, url, headers)
            } else if (statusCode == INVALID_STATUS_CODE) {
                throw HttpException(statusCode)
            } else {
                throw HttpException(urlConnection.responseMessage, statusCode)
            }
        } finally {
            urlConnection.disconnect()
        }
    }

    @Throws(IOException::class)
    private fun getStreamForSuccessfulRequest(
        urlConnection: HttpURLConnection,
        startTime: Long
    ): InputStream? {
        val stream: InputStream
        stream = if (TextUtils.isEmpty(urlConnection.contentEncoding)) {
            val contentLength = urlConnection.contentLength
            ContentLengthInputStream.obtain(urlConnection.inputStream, contentLength.toLong())
        } else {
            if (Log.isLoggable(TAG, Log.DEBUG)) {
                Log.d(TAG, "Got non empty content encoding: " + urlConnection.contentEncoding)
            }
            urlConnection.inputStream
        }
        val contentLength = urlConnection.contentLength
        if (contentLength <= 0) {
            stream.close()
            imageMonitor(
                null,
                startTime,
                System.currentTimeMillis(),
                contentLength,
                urlConnection.url.toString(),
                false,
                "contentLength is 0"
            )
            return null
        }
        return try {
            val byteBuffer = ByteBuffer.allocate(contentLength)
            val buf = ByteArray(1024)
            var length =0
            while (!isCancelled && stream.read(buf).also { length = it } > 0) {
                byteBuffer.put(buf, 0, length)
            }
            stream.close()
            if (isCancelled) {
                return null
            }
            imageMonitor(
                byteBuffer,
                startTime,
                System.currentTimeMillis(),
                contentLength,
                urlConnection.url.toString(),
                true,
                ""
            )
            return ByteArrayInputStream(byteBuffer.array(), 0, contentLength)
        } catch (e: Throwable) {
            val msg = Log.getStackTraceString(e)
            imageMonitor(
                null,
                startTime,
                System.currentTimeMillis(),
                contentLength,
                urlConnection.url.toString(),
                false,
                msg
            )
            null
        }
    }

    /**
     * 监控上报
     */
    private fun imageMonitor(
        byteBuffer: ByteBuffer?,
        startTime: Long,
        endTime: Long,
        contentLength: Int,
        url: String,
        success: Boolean,
        msg: String
    ) {
        try {
            ImageMonitor.monitorResult(
                byteBuffer,
                startTime,
                endTime,
                contentLength,
                url,
                success,
                msg
            )
        } catch (e: Exception) {
            e.printStackTrace()
        }
    }

    override fun cleanup() {}
    override fun cancel() {
        isCancelled = true
        job?.cancel()
        job = null
    }

    override fun getDataClass() =InputStream::class.java

    override fun getDataSource() = DataSource.REMOTE

    interface HttpUrlConnectionFactory {
        @Throws(IOException::class)
        fun build(url: URL): HttpURLConnection
    }

    class DefaultHttpUrlConnectionFactory @Synthetic internal constructor() :
        HttpUrlConnectionFactory {
        @Throws(IOException::class)
        override fun build(url: URL): HttpURLConnection {
            return url.openConnection() as HttpURLConnection
        }
    }

    companion object {
        const val TAG = "GlidUrlFetcher"
        const val MAXIMUM_REDIRECTS = 5

        @VisibleForTesting
        val DEFAULT_CONNECTION_FACTORY: HttpUrlConnectionFactory = DefaultHttpUrlConnectionFactory()

        /**
         * Returned when a connection error prevented us from receiving an http error.
         */
        const val INVALID_STATUS_CODE = -1

        // Referencing constants is less clear than a simple static method.
        fun isHttpOk(statusCode: Int): Boolean {
            return statusCode / 100 == 2
        }

        // Referencing constants is less clear than a simple static method.
        fun isHttpRedirect(statusCode: Int): Boolean {
            return statusCode / 100 == 3
        }
    }
}